/* GStreamer
 * Copyright (C) 2010 Wim Taymans <wim.taymans at gmail.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */
/**
 * SECTION:rtsp-auth
 * @short_description: Authentication and authorization
 * @see_also: #GstRTSPPermissions, #GstRTSPToken
 *
 * The #GstRTSPAuth object is responsible for checking if the current user is
 * allowed to perform requested actions. The default implementation has some
 * reasonable checks but subclasses can implement custom security policies.
 *
 * A new auth object is made with gst_rtsp_auth_new(). It is usually configured
 * on the #GstRTSPServer object.
 *
 * The RTSP server will call gst_rtsp_auth_check() with a string describing the
 * check to perform. The possible checks are prefixed with
 * GST_RTSP_AUTH_CHECK_*. Depending on the check, the default implementation
 * will use the current #GstRTSPToken, #GstRTSPContext and
 * #GstRTSPPermissions on the object to check if an operation is allowed.
 *
 * The default #GstRTSPAuth object has support for basic authentication. With
 * gst_rtsp_auth_add_basic() you can add a basic authentication string together
 * with the #GstRTSPToken that will become active when successfully
 * authenticated.
 *
 * When a TLS certificate has been set with gst_rtsp_auth_set_tls_certificate(),
 * the default auth object will require the client to connect with a TLS
 * connection.
 *
 * Last reviewed on 2013-07-16 (1.0.0)
 */
/*
* ChangeLog:
*
* 2019/10/01 Improved authentication security by Takeshi Yonezawa.
*/

#include <string.h>

#include "rtsp-auth.h"

#define GST_RTSP_AUTH_GET_PRIVATE(obj)  \
   (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GST_TYPE_RTSP_AUTH, GstRTSPAuthPrivate))

typedef struct
{
  GstRTSPToken *token;
  gchar *pass;
  gchar *user;
} GstRTSPDigestEntry;

struct _GstRTSPAuthPrivate
{
  GMutex lock;

  /* the TLS certificate */
  GTlsCertificate *certificate;
  GTlsDatabase *database;
  GTlsAuthenticationMode mode;
  GHashTable *basic;            /* protected by lock */
  GHashTable *nonces;           /* protected by lock */
  GstRTSPDigestEntry **digest;  /* protected by lock */
  guint64 last_nonce_check;
  GstRTSPToken *default_token;
  GstRTSPMethod methods;
  GstRTSPAuthMethod auth_methods;
  gchar *realm;
  gint users;
  gsize user_len;
  gint32 min_pass_len;
  gint32 max_pass_len;
};

typedef struct
{
  gchar *nonce;
  gchar *ip;
  guint64 timestamp;
  gpointer client;
  guint32 nc;
} GstRTSPDigestNonce;

static void
gst_rtsp_digest_entry_free (GstRTSPDigestEntry * entry)
{
  gst_rtsp_token_unref (entry->token);
  g_free (entry->pass);
  g_free (entry->user);
  g_free (entry);
}

static void
gst_rtsp_digest_nonce_free (GstRTSPDigestNonce * nonce)
{
  g_free (nonce->nonce);
  g_free (nonce->ip);
  g_free (nonce);
}

static void
gst_rtsp_digest_table_free (GstRTSPAuth * auth)
{
  GstRTSPAuthPrivate *priv;
  gsize i;

  priv = auth->priv;

  if (priv->digest) {
    for (i = 0; i < priv->users; i++) {
      if (priv->digest[i] != NULL)
        gst_rtsp_digest_entry_free (priv->digest[i]);
    }
    g_free (priv->digest);
    priv->digest = NULL;
    priv->users = 0;
    priv->user_len = 0;
  }
}

enum
{
  PROP_0,
  PROP_LAST
};

enum
{
  SIGNAL_ACCEPT_CERTIFICATE,
  SIGNAL_AUTH_IS_ALLOWED,
  SIGNAL_AUTH_REFUSED,
  SIGNAL_LAST
};

static guint signals[SIGNAL_LAST] = { 0 };

GST_DEBUG_CATEGORY_STATIC (rtsp_auth_debug);
#define GST_CAT_DEFAULT rtsp_auth_debug

static void gst_rtsp_auth_get_property (GObject * object, guint propid,
    GValue * value, GParamSpec * pspec);
static void gst_rtsp_auth_set_property (GObject * object, guint propid,
    const GValue * value, GParamSpec * pspec);
static void gst_rtsp_auth_finalize (GObject * obj);

static gpointer digest_table_lookup (GstRTSPAuth * auth, const gchar * user);
static gint compare (const gchar * s1, const gchar * s2, gsize n);
static gchar * generate_random_string (gint min, gint max);

static gboolean default_authenticate (GstRTSPAuth * auth, GstRTSPContext * ctx);
static gboolean default_check (GstRTSPAuth * auth, GstRTSPContext * ctx,
    const gchar * check);
static void default_generate_authenticate_header (GstRTSPAuth * auth,
    GstRTSPContext * ctx);
static gboolean default_auth_is_allowed (GstRTSPAuth * auth,
    GstRTSPContext * ctx);

static gboolean
is_allowed_signal_accumulator (GSignalInvocationHint * ihint, GValue * return_accu,
    const GValue * handler_return, gpointer data);


G_DEFINE_TYPE (GstRTSPAuth, gst_rtsp_auth, G_TYPE_OBJECT);

static void
gst_rtsp_auth_class_init (GstRTSPAuthClass * klass)
{
  GObjectClass *gobject_class;

  g_type_class_add_private (klass, sizeof (GstRTSPAuthPrivate));

  gobject_class = G_OBJECT_CLASS (klass);

  gobject_class->get_property = gst_rtsp_auth_get_property;
  gobject_class->set_property = gst_rtsp_auth_set_property;
  gobject_class->finalize = gst_rtsp_auth_finalize;

  klass->authenticate = default_authenticate;
  klass->check = default_check;
  klass->generate_authenticate_header = default_generate_authenticate_header;
  klass->auth_is_allowed = default_auth_is_allowed;

  GST_DEBUG_CATEGORY_INIT (rtsp_auth_debug, "rtspauth", 0, "GstRTSPAuth");

  /**
   * GstRTSPAuth::accept-certificate:
   * @auth: a #GstRTSPAuth
   * @connection: a #GTlsConnection
   * @peer_cert: the peer's #GTlsCertificate
   * @errors: the problems with @peer_cert.
   *
   * Emitted during the TLS handshake after the client certificate has
   * been received. See also gst_rtsp_auth_set_tls_authentication_mode().
   *
   * Returns: %TRUE to accept @peer_cert (which will also
   * immediately end the signal emission). %FALSE to allow the signal
   * emission to continue, which will cause the handshake to fail if
   * no one else overrides it.
   *
   * Since: 1.6
   */
  signals[SIGNAL_ACCEPT_CERTIFICATE] = g_signal_new ("accept-certificate",
      G_TYPE_FROM_CLASS (gobject_class),
      G_SIGNAL_RUN_LAST,
      G_STRUCT_OFFSET (GstRTSPAuthClass, accept_certificate),
      g_signal_accumulator_true_handled, NULL, g_cclosure_marshal_generic,
      G_TYPE_BOOLEAN, 3, G_TYPE_TLS_CONNECTION, G_TYPE_TLS_CERTIFICATE,
      G_TYPE_TLS_CERTIFICATE_FLAGS);

    signals[SIGNAL_AUTH_IS_ALLOWED] = g_signal_new ("auth-is-allowed",
      G_TYPE_FROM_CLASS (gobject_class),
      G_SIGNAL_RUN_LAST,
      G_STRUCT_OFFSET (GstRTSPAuthClass, auth_is_allowed),
      is_allowed_signal_accumulator, NULL, g_cclosure_marshal_generic,
      G_TYPE_BOOLEAN, 1, GST_TYPE_RTSP_CONTEXT);

    signals[SIGNAL_AUTH_REFUSED] = g_signal_new ("auth-refused",
      G_TYPE_FROM_CLASS (gobject_class),
      G_SIGNAL_RUN_LAST,
      G_STRUCT_OFFSET (GstRTSPAuthClass, auth_refused),
      NULL, NULL, g_cclosure_marshal_generic,
      G_TYPE_NONE, 1, GST_TYPE_RTSP_CONTEXT);
}

static void
gst_rtsp_auth_init (GstRTSPAuth * auth)
{
  GstRTSPAuthPrivate *priv;

  auth->priv = priv = GST_RTSP_AUTH_GET_PRIVATE (auth);

  g_mutex_init (&priv->lock);

  priv->basic = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
      (GDestroyNotify) gst_rtsp_token_unref);
  priv->digest = NULL;
  priv->nonces = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
      (GDestroyNotify) gst_rtsp_digest_nonce_free);

  /* bitwise or of all methods that need authentication */
  priv->methods = 0;
  priv->auth_methods = GST_RTSP_AUTH_BASIC;
  priv->realm = g_strdup ("GStreamer RTSP Server");
  priv->users = 0;
  priv->user_len = 0;
  priv->min_pass_len = 0;
  priv->max_pass_len = 0;
}

static void
gst_rtsp_auth_finalize (GObject * obj)
{
  GstRTSPAuth *auth = GST_RTSP_AUTH (obj);
  GstRTSPAuthPrivate *priv = auth->priv;

  GST_INFO ("finalize auth %p", auth);

  if (priv->certificate)
    g_object_unref (priv->certificate);
  if (priv->database)
    g_object_unref (priv->database);
  g_hash_table_unref (priv->basic);
  gst_rtsp_digest_table_free (auth);
  g_hash_table_unref (priv->nonces);
  g_mutex_clear (&priv->lock);
  g_free (priv->realm);

  G_OBJECT_CLASS (gst_rtsp_auth_parent_class)->finalize (obj);
}

static void
gst_rtsp_auth_get_property (GObject * object, guint propid,
    GValue * value, GParamSpec * pspec)
{
  switch (propid) {
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, propid, pspec);
  }
}

static void
gst_rtsp_auth_set_property (GObject * object, guint propid,
    const GValue * value, GParamSpec * pspec)
{
  switch (propid) {
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, propid, pspec);
  }
}

/**
 * gst_rtsp_auth_new:
 *
 * Create a new #GstRTSPAuth instance.
 *
 * Returns: (transfer full): a new #GstRTSPAuth
 */
GstRTSPAuth *
gst_rtsp_auth_new (void)
{
  GstRTSPAuth *result;

  result = g_object_new (GST_TYPE_RTSP_AUTH, NULL);

  return result;
}

/**
 * gst_rtsp_auth_set_tls_certificate:
 * @auth: a #GstRTSPAuth
 * @cert: (transfer none) (allow-none): a #GTlsCertificate
 *
 * Set the TLS certificate for the auth. Client connections will only
 * be accepted when TLS is negotiated.
 */
void
gst_rtsp_auth_set_tls_certificate (GstRTSPAuth * auth, GTlsCertificate * cert)
{
  GstRTSPAuthPrivate *priv;
  GTlsCertificate *old;

  g_return_if_fail (GST_IS_RTSP_AUTH (auth));

  priv = auth->priv;

  if (cert)
    g_object_ref (cert);

  g_mutex_lock (&priv->lock);
  old = priv->certificate;
  priv->certificate = cert;
  g_mutex_unlock (&priv->lock);

  if (old)
    g_object_unref (old);
}

/**
 * gst_rtsp_auth_get_tls_certificate:
 * @auth: a #GstRTSPAuth
 *
 * Get the #GTlsCertificate used for negotiating TLS @auth.
 *
 * Returns: (transfer full): the #GTlsCertificate of @auth. g_object_unref() after
 * usage.
 */
GTlsCertificate *
gst_rtsp_auth_get_tls_certificate (GstRTSPAuth * auth)
{
  GstRTSPAuthPrivate *priv;
  GTlsCertificate *result;

  g_return_val_if_fail (GST_IS_RTSP_AUTH (auth), NULL);

  priv = auth->priv;

  g_mutex_lock (&priv->lock);
  if ((result = priv->certificate))
    g_object_ref (result);
  g_mutex_unlock (&priv->lock);

  return result;
}

/**
 * gst_rtsp_auth_set_tls_database:
 * @auth: a #GstRTSPAuth
 * @database: (transfer none) (allow-none): a #GTlsDatabase
 *
 * Sets the certificate database that is used to verify peer certificates.
 * If set to %NULL (the default), then peer certificate validation will always
 * set the %G_TLS_CERTIFICATE_UNKNOWN_CA error.
 *
 * Since 1.6
 */
void
gst_rtsp_auth_set_tls_database (GstRTSPAuth * auth, GTlsDatabase * database)
{
  GstRTSPAuthPrivate *priv;
  GTlsDatabase *old;

  g_return_if_fail (GST_IS_RTSP_AUTH (auth));

  priv = auth->priv;

  if (database)
    g_object_ref (database);

  g_mutex_lock (&priv->lock);
  old = priv->database;
  priv->database = database;
  g_mutex_unlock (&priv->lock);

  if (old)
    g_object_unref (old);
}

/**
 * gst_rtsp_auth_get_tls_database:
 * @auth: a #GstRTSPAuth
 *
 * Get the #GTlsDatabase used for verifying client certificate.
 *
 * Returns: (transfer full): the #GTlsDatabase of @auth. g_object_unref() after
 * usage.
 * Since: 1.6
 */
GTlsDatabase *
gst_rtsp_auth_get_tls_database (GstRTSPAuth * auth)
{
  GstRTSPAuthPrivate *priv;
  GTlsDatabase *result;

  g_return_val_if_fail (GST_IS_RTSP_AUTH (auth), NULL);

  priv = auth->priv;

  g_mutex_lock (&priv->lock);
  if ((result = priv->database))
    g_object_ref (result);
  g_mutex_unlock (&priv->lock);

  return result;
}

/**
 * gst_rtsp_auth_set_tls_authentication_mode:
 * @auth: a #GstRTSPAuth
 * @mode: (transfer none) (allow-none): a #GTlsAuthenticationMode
 *
 * The #GTlsAuthenticationMode to set on the underlying GTlsServerConnection.
 * When set to another value than %G_TLS_AUTHENTICATION_NONE,
 * #GstRTSPAuth::accept-certificate signal will be emitted and must be handled.
 *
 * Since: 1.6
 */
void
gst_rtsp_auth_set_tls_authentication_mode (GstRTSPAuth * auth,
    GTlsAuthenticationMode mode)
{
  GstRTSPAuthPrivate *priv;

  g_return_if_fail (GST_IS_RTSP_AUTH (auth));

  priv = auth->priv;

  g_mutex_lock (&priv->lock);
  priv->mode = mode;
  g_mutex_unlock (&priv->lock);
}

/**
 * gst_rtsp_auth_get_tls_authentication_mode:
 * @auth: a #GstRTSPAuth
 *
 * Get the #GTlsAuthenticationMode.
 *
 * Returns: (transfer full): the #GTlsAuthenticationMode.
 */
GTlsAuthenticationMode
gst_rtsp_auth_get_tls_authentication_mode (GstRTSPAuth * auth)
{
  GstRTSPAuthPrivate *priv;
  GTlsAuthenticationMode result;

  g_return_val_if_fail (GST_IS_RTSP_AUTH (auth), G_TLS_AUTHENTICATION_NONE);

  priv = auth->priv;

  g_mutex_lock (&priv->lock);
  result = priv->mode;
  g_mutex_unlock (&priv->lock);

  return result;
}

/**
 * gst_rtsp_auth_set_default_token:
 * @auth: a #GstRTSPAuth
 * @token: (transfer none) (allow-none): a #GstRTSPToken
 *
 * Set the default #GstRTSPToken to @token in @auth. The default token will
 * be used for unauthenticated users.
 */
void
gst_rtsp_auth_set_default_token (GstRTSPAuth * auth, GstRTSPToken * token)
{
  GstRTSPAuthPrivate *priv;
  GstRTSPToken *old;

  g_return_if_fail (GST_IS_RTSP_AUTH (auth));

  priv = auth->priv;

  if (token)
    gst_rtsp_token_ref (token);

  g_mutex_lock (&priv->lock);
  old = priv->default_token;
  priv->default_token = token;
  g_mutex_unlock (&priv->lock);

  if (old)
    gst_rtsp_token_unref (old);
}

/**
 * gst_rtsp_auth_get_default_token:
 * @auth: a #GstRTSPAuth
 *
 * Get the default token for @auth. This token will be used for unauthenticated
 * users.
 *
 * Returns: (transfer full): the #GstRTSPToken of @auth. gst_rtsp_token_unref() after
 * usage.
 */
GstRTSPToken *
gst_rtsp_auth_get_default_token (GstRTSPAuth * auth)
{
  GstRTSPAuthPrivate *priv;
  GstRTSPToken *result;

  g_return_val_if_fail (GST_IS_RTSP_AUTH (auth), NULL);

  priv = auth->priv;

  g_mutex_lock (&priv->lock);
  if ((result = priv->default_token))
    gst_rtsp_token_ref (result);
  g_mutex_unlock (&priv->lock);

  return result;
}

/**
 * gst_rtsp_auth_add_basic:
 * @auth: a #GstRTSPAuth
 * @basic: the basic token
 * @token: (transfer none): authorisation token
 *
 * Add a basic token for the default authentication algorithm that
 * enables the client with privileges listed in @token.
 */
void
gst_rtsp_auth_add_basic (GstRTSPAuth * auth, const gchar * basic,
    GstRTSPToken * token)
{
  GstRTSPAuthPrivate *priv;

  g_return_if_fail (GST_IS_RTSP_AUTH (auth));
  g_return_if_fail (basic != NULL);
  g_return_if_fail (GST_IS_RTSP_TOKEN (token));

  priv = auth->priv;

  g_mutex_lock (&priv->lock);
  g_hash_table_replace (priv->basic, g_strdup (basic),
      gst_rtsp_token_ref (token));
  g_mutex_unlock (&priv->lock);
}

/**
 * gst_rtsp_auth_remove_basic:
 * @auth: a #GstRTSPAuth
 * @basic: (transfer none): the basic token
 *
 * Removes @basic authentication token.
 */
void
gst_rtsp_auth_remove_basic (GstRTSPAuth * auth, const gchar * basic)
{
  GstRTSPAuthPrivate *priv;

  g_return_if_fail (GST_IS_RTSP_AUTH (auth));
  g_return_if_fail (basic != NULL);

  priv = auth->priv;

  g_mutex_lock (&priv->lock);
  g_hash_table_remove (priv->basic, basic);
  g_mutex_unlock (&priv->lock);
}

/**
 * gst_rtsp_auth_alloc_digest_users:
 * @auth: a #GstRTSPAuth
 * @users: max count of entry users
 * @len: max length of user name
 *
 * Allocate of digest user tables.
 */
void
gst_rtsp_auth_alloc_digest_users (GstRTSPAuth * auth, gint users, gsize len)
{
  GstRTSPAuthPrivate *priv;
  gsize i;

  g_return_if_fail (GST_IS_RTSP_AUTH (auth));
  g_return_if_fail (users != 0);
  g_return_if_fail (len != 0);

  priv = auth->priv;

  g_mutex_lock (&priv->lock);
  gst_rtsp_digest_table_free (auth);
  priv->digest = (GstRTSPDigestEntry **) g_malloc (sizeof(GstRTSPDigestEntry *) * users);
  g_assert (priv->digest != NULL);
  for (i = 0; i < users; i++)
    priv->digest[i] = NULL;
  priv->users = users;
  priv->user_len = len;
  g_mutex_unlock (&priv->lock);
}

/**
 * gst_rtsp_auth_add_digest:
 * @auth: a #GstRTSPAuth
 * @user: the digest user name
 * @pass: the digest password
 * @token: (transfer none): authorisation token
 *
 * Add a digest @user and @pass for the default authentication algorithm that
 * enables the client with privileges listed in @token.
 *
 * Since: 1.12
 */
void
gst_rtsp_auth_add_digest (GstRTSPAuth * auth, const gchar * user,
    const gchar * pass, GstRTSPToken * token)
{
  GstRTSPAuthPrivate *priv;
  GstRTSPDigestEntry *entry;
  gsize i;

  g_return_if_fail (GST_IS_RTSP_AUTH (auth));
  g_return_if_fail (user != NULL);
  g_return_if_fail (pass != NULL);
  g_return_if_fail (GST_IS_RTSP_TOKEN (token));
  g_return_if_fail (auth->priv->digest != NULL);

  priv = auth->priv;

  g_mutex_lock (&priv->lock);
  for (i = 0; i < priv->users; i++) {
    if (priv->digest[i] == NULL) {
      entry = g_new0 (GstRTSPDigestEntry, 1);
      entry->token = gst_rtsp_token_ref (token);
      entry->pass = g_strdup (pass);
      entry->user = g_strdup (user);
      priv->digest[i] = entry;
      break;
    }
  }
  g_mutex_unlock (&priv->lock);
}

/**
 * gst_rtsp_auth_remove_digest:
 * @auth: a #GstRTSPAuth
 * @user: (transfer none): the digest user name
 *
 * Removes a digest user.
 *
 * Since: 1.12
 */
void
gst_rtsp_auth_remove_digest (GstRTSPAuth * auth, const gchar * user)
{
  GstRTSPAuthPrivate *priv;
  gsize i;

  g_return_if_fail (GST_IS_RTSP_AUTH (auth));
  g_return_if_fail (user != NULL);
  g_return_if_fail (auth->priv->digest != NULL);

  priv = auth->priv;

  g_mutex_lock (&priv->lock);
  for (i = 0; i < priv->users; i++) {
    if (priv->digest[i] != NULL) {
      if (strcmp (priv->digest[i]->user, user) == 0) {
        gst_rtsp_digest_entry_free (priv->digest[i]);
        priv->digest[i] = NULL;
        break;
      }
    }
  }
  g_mutex_unlock (&priv->lock);
}

/**
 * gst_rtsp_auth_set_supported_methods:
 * @auth: a #GstRTSPAuth
 * @methods: supported methods
 *
 * Sets the supported authentication @methods for @auth.
 *
 * Since: 1.12
 */
void
gst_rtsp_auth_set_supported_methods (GstRTSPAuth * auth,
    GstRTSPAuthMethod methods)
{
  GstRTSPAuthPrivate *priv;

  g_return_if_fail (GST_IS_RTSP_AUTH (auth));

  priv = auth->priv;

  g_mutex_lock (&priv->lock);
  priv->auth_methods = methods;
  g_mutex_unlock (&priv->lock);
}

/**
 * gst_rtsp_auth_get_supported_methods:
 * @auth: a #GstRTSPAuth
 *
 * Gets the supported authentication methods of @auth.
 *
 * Returns: The supported authentication methods
 *
 * Since: 1.12
 */
GstRTSPAuthMethod
gst_rtsp_auth_get_supported_methods (GstRTSPAuth * auth)
{
  GstRTSPAuthPrivate *priv;
  GstRTSPAuthMethod methods;

  g_return_val_if_fail (GST_IS_RTSP_AUTH (auth), 0);

  priv = auth->priv;

  g_mutex_lock (&priv->lock);
  methods = priv->auth_methods;
  g_mutex_unlock (&priv->lock);

  return methods;
}

typedef struct
{
  GstRTSPAuth *auth;
  GstRTSPDigestNonce *nonce;
} RemoveNonceData;

static void
remove_nonce (gpointer data, GObject * object)
{
  RemoveNonceData *remove_nonce_data = data;

  g_mutex_lock (&remove_nonce_data->auth->priv->lock);
  g_hash_table_remove (remove_nonce_data->auth->priv->nonces,
      remove_nonce_data->nonce->nonce);
  g_mutex_unlock (&remove_nonce_data->auth->priv->lock);

  g_object_unref (remove_nonce_data->auth);
  g_free (remove_nonce_data);
}

static gboolean
default_digest_auth (GstRTSPAuth * auth, GstRTSPContext * ctx,
    GstRTSPAuthParam ** param)
{
  const gchar *realm = NULL, *user = NULL, *nonce = NULL;
  const gchar *response = NULL, *uri = NULL;
  const gchar *algorithm = NULL, *qop = NULL;
  const gchar *cnonce = NULL, *nc = NULL;
  GstRTSPDigestNonce *nonce_entry = NULL;
  GstRTSPDigestEntry *digest_entry;
  gchar *expected_response = NULL;
  gchar *random_string = NULL, *pass = NULL;
  gboolean ret = FALSE;

  GST_DEBUG_OBJECT (auth, "check Digest auth");

  if (!param)
    return ret;

  while (*param) {
    if (!realm && strcmp ((*param)->name, "realm") == 0 && (*param)->value)
      realm = (*param)->value;
    else if (!user && strcmp ((*param)->name, "username") == 0
        && (*param)->value)
      user = (*param)->value;
    else if (!nonce && strcmp ((*param)->name, "nonce") == 0 && (*param)->value)
      nonce = (*param)->value;
    else if (!response && strcmp ((*param)->name, "response") == 0
        && (*param)->value)
      response = (*param)->value;
    else if (!uri && strcmp ((*param)->name, "uri") == 0 && (*param)->value)
      uri = (*param)->value;
    else if (!algorithm && strcmp ((*param)->name, "algorithm") == 0
        && (*param)->value)
      algorithm = (*param)->value;
    else if (!qop && strcmp ((*param)->name, "qop") == 0 && (*param)->value)
      qop = (*param)->value;
    else if (!cnonce && strcmp ((*param)->name, "cnonce") == 0
        && (*param)->value)
      cnonce = (*param)->value;
    else if (!nc && strcmp ((*param)->name, "nc") == 0 && (*param)->value)
      nc = (*param)->value;

    param++;
  }

  if (!realm || !user || !nonce || !response || !uri)
    return FALSE;

  g_mutex_lock (&auth->priv->lock);
  if (strlen (user) == 0)
    goto out;

  digest_entry = digest_table_lookup (auth, user);
  nonce_entry = g_hash_table_lookup (auth->priv->nonces, nonce);
  if (!nonce_entry)
    goto out;

  if (strcmp (nonce_entry->ip, gst_rtsp_connection_get_ip (ctx->conn)) != 0)
    goto out;
  if (nonce_entry->client && nonce_entry->client != ctx->client)
    goto out;

  random_string = generate_random_string (auth->priv->min_pass_len, auth->priv->max_pass_len);
  pass = (digest_entry) ? digest_entry->pass : random_string;

  if (algorithm && qop) {
    if (nc && nonce_entry->nc + 1 == atoi (nc))
      nonce_entry->nc++;
    else
      nc = random_string;

    expected_response =
        gst_rtsp_generate_digest_auth_response_secure (
        algorithm, gst_rtsp_method_as_text (ctx->method),
        realm, user, pass, uri,
        nonce, nc, cnonce, qop, NULL);
  }
  else
    expected_response =
        gst_rtsp_generate_digest_auth_response (NULL,
        gst_rtsp_method_as_text (ctx->method), realm, user,
        pass, uri, nonce);

  ret = (expected_response && compare (response, expected_response, strlen (response)) == 0 && digest_entry);
  if (ret)
    g_signal_emit (auth, signals[SIGNAL_AUTH_IS_ALLOWED], 0,
        ctx, &ret);
  ctx->token = (ret) ? digest_entry->token : NULL;

out:
  if (nonce_entry && !nonce_entry->client) {
    RemoveNonceData *remove_nonce_data = g_new (RemoveNonceData, 1);

    nonce_entry->client = ctx->client;
    remove_nonce_data->nonce = nonce_entry;
    remove_nonce_data->auth = g_object_ref (auth);
    g_object_weak_ref (G_OBJECT (ctx->client), remove_nonce, remove_nonce_data);
  }
  g_mutex_unlock (&auth->priv->lock);

  g_free (expected_response);
  g_free (random_string);

  return ret;
}

static gpointer
digest_table_lookup (GstRTSPAuth * auth, const gchar * user)
{
  GstRTSPAuthPrivate *priv;
  gsize i;
  gpointer entry;

  priv = auth->priv;
  entry = NULL;

  for (i = 0; i < priv->users; i++) {
    if (compare (user, (priv->digest[i] != NULL)
      ? priv->digest[i]->user : "", priv->user_len) == 0)
      entry = priv->digest[i];
  }
  return entry;
}

static gint
compare (const gchar * s1, const gchar * s2, gsize n)
{
  volatile gsize i, j;
  volatile gchar dmy;

  for (i = 0; i < n; i++) {
    if (s1[i] == s2[i]) {
      if (s1[i] == '\0')
        goto bail;
    } else {
      if (dmy == '\0')
        ;
      goto bail;
    }
  }
bail:
  for (j = i; j < n; j++) {
    if (dmy == dmy) {
      if (dmy == '\0')
        ;
    }
  }
  return (s1[i] - s2[i]);
}

static gchar *
generate_random_string (gint min, gint max)
{
  gint i;
  gint len;
  gchar *str;

  g_return_val_if_fail (min > 0, NULL);
  g_return_val_if_fail (min < max, NULL);

  len = g_random_int_range (min, max + 1);
  str = g_strndup ("", len);

  for (i = 0; i < len; i++)
    str[i] = (gchar)g_random_int_range (0x20, 0x7f);

  str[len] = '\0';
  return str;
}

static gboolean
default_authenticate (GstRTSPAuth * auth, GstRTSPContext * ctx)
{
  GstRTSPAuthPrivate *priv = auth->priv;
  GstRTSPAuthCredential **credentials, **credential;

  GST_DEBUG_OBJECT (auth, "authenticate");

  g_mutex_lock (&priv->lock);
  /* FIXME, need to ref but we have no way to unref when the ctx is
   * popped */
  ctx->token = priv->default_token;
  g_mutex_unlock (&priv->lock);

  credentials =
      gst_rtsp_message_parse_auth_credentials (ctx->request,
      GST_RTSP_HDR_AUTHORIZATION);
  if (!credentials)
    goto no_auth;

  /* parse type */
  credential = credentials;
  while (*credential) {
    if ((*credential)->scheme == GST_RTSP_AUTH_BASIC) {
      GstRTSPToken *token;

      GST_DEBUG_OBJECT (auth, "check Basic auth");
      g_mutex_lock (&priv->lock);
      if ((token =
              g_hash_table_lookup (priv->basic,
                  (*credential)->authorization))) {
        GST_DEBUG_OBJECT (auth, "setting token %p", token);
        ctx->token = token;
        g_mutex_unlock (&priv->lock);
        break;
      }
      g_mutex_unlock (&priv->lock);
    } else if ((*credential)->scheme == GST_RTSP_AUTH_DIGEST) {
      if (default_digest_auth (auth, ctx, (*credential)->params))
        break;
    }

    credential++;
  }

  gst_rtsp_auth_credentials_free (credentials);
  return TRUE;

no_auth:
  {
    GST_DEBUG_OBJECT (auth, "no authorization header found");
    return TRUE;
  }
}

static void
default_generate_authenticate_header (GstRTSPAuth * auth, GstRTSPContext * ctx)
{
  if (auth->priv->auth_methods & GST_RTSP_AUTH_BASIC) {
    gchar *auth_header =
        g_strdup_printf ("Basic realm=\"%s\"", auth->priv->realm);
    gst_rtsp_message_add_header (ctx->response, GST_RTSP_HDR_WWW_AUTHENTICATE,
        auth_header);
    g_free (auth_header);
  }

  if (auth->priv->auth_methods & GST_RTSP_AUTH_DIGEST) {
    GstRTSPDigestNonce *nonce;
    gchar *nonce_value, *auth_header;

    nonce_value =
        g_strdup_printf ("%08x%08x", g_random_int (), g_random_int ());

    auth_header =
        g_strdup_printf
        ("Digest realm=\"%s\", nonce=\"%s\", algorithm=\"MD5\", qop=\"auth\"", auth->priv->realm, nonce_value);
    gst_rtsp_message_add_header (ctx->response, GST_RTSP_HDR_WWW_AUTHENTICATE,
        auth_header);
    g_free (auth_header);

    nonce = g_new0 (GstRTSPDigestNonce, 1);
    nonce->nonce = g_strdup (nonce_value);
    nonce->timestamp = g_get_monotonic_time ();
    nonce->ip = g_strdup (gst_rtsp_connection_get_ip (ctx->conn));
    nonce->nc = 0;
    g_mutex_lock (&auth->priv->lock);
    g_hash_table_replace (auth->priv->nonces, nonce_value, nonce);

    if (auth->priv->last_nonce_check == 0)
      auth->priv->last_nonce_check = nonce->timestamp;

    /* 30 second nonce timeout */
    if (nonce->timestamp - auth->priv->last_nonce_check >= 30 * G_USEC_PER_SEC) {
      GHashTableIter iter;
      gpointer key, value;

      g_hash_table_iter_init (&iter, auth->priv->nonces);
      while (g_hash_table_iter_next (&iter, &key, &value)) {
        GstRTSPDigestNonce *tmp = value;

        if (!tmp->client
            && nonce->timestamp - tmp->timestamp >= 30 * G_USEC_PER_SEC)
          g_hash_table_iter_remove (&iter);
      }
      auth->priv->last_nonce_check = nonce->timestamp;
    }

    g_mutex_unlock (&auth->priv->lock);
  }
}

static gboolean
default_auth_is_allowed (GstRTSPAuth * auth, GstRTSPContext * ctx)
{
  GST_DEBUG_OBJECT (auth, "auth is allowed");
  return TRUE;
}

static gboolean
is_allowed_signal_accumulator (GSignalInvocationHint * ihint, GValue * return_accu,
    const GValue * handler_return, gpointer data)
{
  gboolean handler_value = g_value_get_boolean (handler_return);

  g_value_set_boolean (return_accu, handler_value);

  return FALSE;
}

static void
send_response (GstRTSPAuth * auth, GstRTSPStatusCode code, GstRTSPContext * ctx)
{
  gst_rtsp_message_init_response (ctx->response, code,
      gst_rtsp_status_as_text (code), ctx->request);

  if (code == GST_RTSP_STS_UNAUTHORIZED) {
    GstRTSPAuthClass *klass;

    klass = GST_RTSP_AUTH_GET_CLASS (auth);

    if (klass->generate_authenticate_header)
      klass->generate_authenticate_header (auth, ctx);
  }
  gst_rtsp_client_send_message (ctx->client, ctx->session, ctx->response);
}

static gboolean
ensure_authenticated (GstRTSPAuth * auth, GstRTSPContext * ctx)
{
  GstRTSPAuthClass *klass;

  klass = GST_RTSP_AUTH_GET_CLASS (auth);

  /* we need a token to check */
  if (ctx->token == NULL) {
    if (klass->authenticate) {
      if (!klass->authenticate (auth, ctx))
        goto authenticate_failed;
    }
  }
  if (ctx->token == NULL)
    goto no_auth;

  return TRUE;

/* ERRORS */
authenticate_failed:
  {
    GST_DEBUG_OBJECT (auth, "authenticate failed");
    g_signal_emit (auth, signals[SIGNAL_AUTH_REFUSED], 0,
        ctx);
    send_response (auth, GST_RTSP_STS_UNAUTHORIZED, ctx);
    return FALSE;
  }
no_auth:
  {
    GST_DEBUG_OBJECT (auth, "no authorization token found");
    g_signal_emit (auth, signals[SIGNAL_AUTH_REFUSED], 0,
        ctx);
    send_response (auth, GST_RTSP_STS_UNAUTHORIZED, ctx);
    return FALSE;
  }
}

static gboolean
accept_certificate_cb (GTlsConnection * conn, GTlsCertificate * peer_cert,
    GTlsCertificateFlags errors, GstRTSPAuth * auth)
{
  gboolean ret = FALSE;

  g_signal_emit (auth, signals[SIGNAL_ACCEPT_CERTIFICATE], 0,
      conn, peer_cert, errors, &ret);

  return ret;
}

/* new connection */
static gboolean
check_connect (GstRTSPAuth * auth, GstRTSPContext * ctx, const gchar * check)
{
  GstRTSPAuthPrivate *priv = auth->priv;
  GTlsConnection *tls;

  /* configure the connection */

  if (priv->certificate) {
    tls = gst_rtsp_connection_get_tls (ctx->conn, NULL);
    g_tls_connection_set_certificate (tls, priv->certificate);
  }

  if (priv->mode != G_TLS_AUTHENTICATION_NONE) {
    tls = gst_rtsp_connection_get_tls (ctx->conn, NULL);
    g_tls_connection_set_database (tls, priv->database);
    g_object_set (tls, "authentication-mode", priv->mode, NULL);
    g_signal_connect (tls, "accept-certificate",
        G_CALLBACK (accept_certificate_cb), auth);
  }

  return TRUE;
}

/* check url and methods */
static gboolean
check_url (GstRTSPAuth * auth, GstRTSPContext * ctx, const gchar * check)
{
  GstRTSPAuthPrivate *priv = auth->priv;

  if ((ctx->method & priv->methods) != 0)
    if (!ensure_authenticated (auth, ctx))
      goto not_authenticated;

  return TRUE;

  /* ERRORS */
not_authenticated:
  {
    return FALSE;
  }
}

/* check access to media factory */
static gboolean
check_factory (GstRTSPAuth * auth, GstRTSPContext * ctx, const gchar * check)
{
  const gchar *role;
  GstRTSPPermissions *perms;

  if (!ensure_authenticated (auth, ctx))
    return FALSE;

  if (!(role = gst_rtsp_token_get_string (ctx->token,
              GST_RTSP_TOKEN_MEDIA_FACTORY_ROLE)))
    goto no_media_role;
  if (!(perms = gst_rtsp_media_factory_get_permissions (ctx->factory)))
    goto no_permissions;

  if (g_str_equal (check, GST_RTSP_AUTH_CHECK_MEDIA_FACTORY_ACCESS)) {
    if (!gst_rtsp_permissions_is_allowed (perms, role,
            GST_RTSP_PERM_MEDIA_FACTORY_ACCESS))
      goto no_access;
  } else if (g_str_equal (check, GST_RTSP_AUTH_CHECK_MEDIA_FACTORY_CONSTRUCT)) {
    if (!gst_rtsp_permissions_is_allowed (perms, role,
            GST_RTSP_PERM_MEDIA_FACTORY_CONSTRUCT))
      goto no_construct;
  }

  gst_rtsp_permissions_unref (perms);

  return TRUE;

  /* ERRORS */
no_media_role:
  {
    GST_DEBUG_OBJECT (auth, "no media factory role found");
    send_response (auth, GST_RTSP_STS_UNAUTHORIZED, ctx);
    return FALSE;
  }
no_permissions:
  {
    GST_DEBUG_OBJECT (auth, "no permissions on media factory found");
    send_response (auth, GST_RTSP_STS_UNAUTHORIZED, ctx);
    return FALSE;
  }
no_access:
  {
    GST_DEBUG_OBJECT (auth, "no permissions to access media factory");
    gst_rtsp_permissions_unref (perms);
    send_response (auth, GST_RTSP_STS_NOT_FOUND, ctx);
    return FALSE;
  }
no_construct:
  {
    GST_DEBUG_OBJECT (auth, "no permissions to construct media factory");
    gst_rtsp_permissions_unref (perms);
    send_response (auth, GST_RTSP_STS_UNAUTHORIZED, ctx);
    return FALSE;
  }
}

static gboolean
check_client_settings (GstRTSPAuth * auth, GstRTSPContext * ctx,
    const gchar * check)
{
  if (!ensure_authenticated (auth, ctx))
    return FALSE;

  return gst_rtsp_token_is_allowed (ctx->token,
      GST_RTSP_TOKEN_TRANSPORT_CLIENT_SETTINGS);
}

static gboolean
default_check (GstRTSPAuth * auth, GstRTSPContext * ctx, const gchar * check)
{
  gboolean res = FALSE;

  /* FIXME, use hastable or so */
  if (g_str_equal (check, GST_RTSP_AUTH_CHECK_CONNECT)) {
    res = check_connect (auth, ctx, check);
  } else if (g_str_equal (check, GST_RTSP_AUTH_CHECK_URL)) {
    res = check_url (auth, ctx, check);
  } else if (g_str_has_prefix (check, "auth.check.media.factory.")) {
    res = check_factory (auth, ctx, check);
  } else if (g_str_equal (check, GST_RTSP_AUTH_CHECK_TRANSPORT_CLIENT_SETTINGS)) {
    res = check_client_settings (auth, ctx, check);
  }
  return res;
}

static gboolean
no_auth_check (const gchar * check)
{
  gboolean res;

  if (g_str_equal (check, GST_RTSP_AUTH_CHECK_TRANSPORT_CLIENT_SETTINGS))
    res = FALSE;
  else
    res = TRUE;

  return res;
}

/**
 * gst_rtsp_auth_check:
 * @check: the item to check
 *
 * Check if @check is allowed in the current context.
 *
 * Returns: FALSE if check failed.
 */
gboolean
gst_rtsp_auth_check (const gchar * check)
{
  gboolean result = FALSE;
  GstRTSPAuthClass *klass;
  GstRTSPContext *ctx;
  GstRTSPAuth *auth;

  g_return_val_if_fail (check != NULL, FALSE);

  if (!(ctx = gst_rtsp_context_get_current ()))
    goto no_context;

  /* no auth, we don't need to check */
  if (!(auth = ctx->auth))
    return no_auth_check (check);

  klass = GST_RTSP_AUTH_GET_CLASS (auth);

  GST_DEBUG_OBJECT (auth, "check authorization '%s'", check);

  if (klass->check)
    result = klass->check (auth, ctx, check);

  return result;

  /* ERRORS */
no_context:
  {
    GST_ERROR ("no context found");
    return FALSE;
  }
}

/**
 * gst_rtsp_auth_make_basic:
 * @user: a userid
 * @pass: a password
 *
 * Construct a Basic authorisation token from @user and @pass.
 *
 * Returns: (transfer full): the base64 encoding of the string @user:@pass.
 * g_free() after usage.
 */
gchar *
gst_rtsp_auth_make_basic (const gchar * user, const gchar * pass)
{
  gchar *user_pass;
  gchar *result;

  g_return_val_if_fail (user != NULL, NULL);
  g_return_val_if_fail (pass != NULL, NULL);

  user_pass = g_strjoin (":", user, pass, NULL);
  result = g_base64_encode ((guchar *) user_pass, strlen (user_pass));
  g_free (user_pass);

  return result;
}

/**
 * gst_rtsp_auth_set_realm:
 *
 * Set the @realm of @auth
 *
 * Since: 1.16
 */
void
gst_rtsp_auth_set_realm (GstRTSPAuth * auth, const gchar * realm)
{
  g_return_if_fail (GST_IS_RTSP_AUTH (auth));
  g_return_if_fail (realm != NULL);

  if (auth->priv->realm)
    g_free (auth->priv->realm);

  auth->priv->realm = g_strdup (realm);
}

/**
 * gst_rtsp_auth_get_realm:
 *
 * Returns: (transfer full): the @realm of @auth
 *
 * Since: 1.16
 */
gchar *
gst_rtsp_auth_get_realm (GstRTSPAuth * auth)
{
  g_return_val_if_fail (GST_IS_RTSP_AUTH (auth), NULL);

  return g_strdup (auth->priv->realm);
}

/**
 * gst_rtsp_auth_set_pass_length:
 * @min: lower closed bound of the interval
 * @max: upper open bound of the interval
 *
 * Set the @min and @max of @auth.
 */
void
gst_rtsp_auth_set_pass_length (GstRTSPAuth *auth, gint min, gint max)
{
  g_return_if_fail (GST_IS_RTSP_AUTH (auth));

  g_mutex_lock (&auth->priv->lock);
  auth->priv->min_pass_len = min;
  auth->priv->max_pass_len = max;
  g_mutex_unlock (&auth->priv->lock);
}
